<?php

/**
 * +--------------------------------------------------------------
 * | eLuoPHP
 * +--------------------------------------------------------------
 * | Copyright (c) 2016-2018 http://qxwoo.cn All rights reserved.
 * +--------------------------------------------------------------
 * | y139w [y139w@163.com]
 * +--------------------------------------------------------------
 * | 数据库连接类文件
 * +--------------------------------------------------------------
 */

namespace luo\db;

use PDO;
use luo\Config;
use luo\exception\PDOException;

abstract class Connection
{

    /**
     * SQL语句被执行后的结果集。
     * @var null
     */
    protected $statement = null;

    /**
     * 当前执行的SQL语句
     * @var null
     */
    protected $sql = null;

    /**
     * 返回结果集类型
     * @var const
     */
    protected $resultType;

    /**
     * 多个数据库连接数组
     * @var array
     */
    protected $links = [];

    /**
     * 当前链接编号
     * @var int
     */
    protected $linkNum = 0;

    /**
     * 数据库配置项数组
     * @var array
     */
    protected $config = [
        // 数据库类型
        'type'    => '',
        // 数据服务器地址
        'host'    => '',
        // 数据库用户名
        'user'    => '',
        // 数据库密码
        'pass'    => '',
        // 数据库名称
        'dbname'  => '',
        // 编码格式
        'charset' => '',
        // 表前缀
        'prefix'  => '',
        // DSN链接
        'dsn'     => '',
        // 返回类型
        'fetch_type' => 'array'
    ];

    /**
     * 返回数据类型
     * @var array
     */
    protected $fetchArray = [
        // 取出为关联索引数组
        'both'        => PDO::FETCH_BOTH,
        // 取出为关联数组
        'array'       => PDO::FETCH_ASSOC,
        // 取出为对象
        'object'      => PDO::FETCH_OBJ,
        // 取出为索引数组
        'number'      => PDO::FETCH_NUM
    ];

    /**
     * PDO属性项
     * @var array
     */
    protected $attrs = [
        // 数据库驱动返回的列名
        PDO::ATTR_CASE               => PDO::CASE_NATURAL,
        // 抛出 exceptions 异常
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        // 不转换null
        PDO::ATTR_ORACLE_NULLS       => PDO::NULL_NATURAL,
        // 提取的时候是否将数值转换为字符串
        PDO::ATTR_STRINGIFY_FETCHES  => false,
        // 是否启用预处理语句的模拟
        PDO::ATTR_EMULATE_PREPARES   => false,
    ];

    /**
     * 构造器
     * Connection constructor.
     * @param Config $config
     * @throws PDOException
     */
    public function __construct(Config $config)
    {
        $this->config = array_merge($this->config, $config::get('database'));
        if(isset($this->config['fetch_type'])){
            $this->resultType = isset($this->fetchArray[$this->config['fetch_type']])
                ? $this->fetchArray[$this->config['fetch_type']]
                : $this->fetchArray['array'];
        }
        $this->links[$this->linkNum] = $this->connect();

    }

    /**
     * DSN组装方法（抽象方法）
     * @param array $config
     * @return mixed
     */
    abstract public function getDsn(array $config);

    /**
     * PDO连接数据库，返回一个PDO对象
     * @param array $config
     * @param int $num
     * @return mixed
     * @throws PDOException
     */
    public function connect(array $config = [], $num = 0){

        try{
            $this->linkNum = $num;
            if(!isset($this->links[$this->linkNum])){
                // 处理配置项
                if(empty($config)){
                    $config = $this->config;
                }else{
                    $config = array_merge($config, $this->config);
                }

                // 连接属性项
                if(isset($config['options']) && is_array($config['options'])){
                    $options = $this->attrs + $config['options'];
                }else{
                    $options = $this->attrs;
                }

                // 调用dsn组装方法得到dsn
                $dsn = $this->getDsn($config);
                // 获取用户名
                $user = $config['user'];
                // 获取登录密码
                $password = $config['pass'];

                if(!isset($this->links[$this->linkNum])){
                    // 实例化得到PDO对象，放入链接数组中。
                    $this->links[$this->linkNum] = new PDO($dsn, $user, $password, $options);
                }

            }

        } catch (\PDOException $e){
            // PDO异常处理
            throw new PDOException($e);
        }
        return $this->links[$this->linkNum];

    }

    /**
     * 释放结果集
     */
    public function release(){
        $this->statement = null;
    }

    /**
     * 查询并返回结果
     * @param $sql
     * @param array $bind
     * @return mixed
     * @throws PDOException
     */
    public function query($sql, $bind = []){

        $this->prepare($sql, $bind);
        $this->count = $this->statement->columnCount();

        return $this->statement->fetchAll($this->resultType);

    }

    /**
     * 执行SQL语句，并返回受影响的行数
     * @param $sql
     * @param array $bind
     * @return mixed
     * @throws PDOException
     */
    public function execute($sql, $bind = []){
        $this->prepare($sql, $bind);
        // 返回受上一个 SQL 语句影响的行数
        $rowCount = $this->statement->rowCount();
        return $rowCount;
    }

    /**
     * sql语句预处理
     * @param $sql
     * @param array $bind
     * @return null
     * @throws PDOException
     */
    public function prepare($sql, $bind = []){

        $link = $this->getConnect();
        //释放上一次的查询结果
        if (!empty($this->statement) && $this->statement->queryString != $sql) {
            $this->release();
        }
        // 记录当前SQL语句
        $this->sql = $sql;
        try {
            if(empty($this->statement)){
                $this->statement = $link->prepare($sql);
            }
            $this->bindValue($bind);
            $this->statement->execute();
            return $this->statement;
        } catch (\PDOException $e) {
            throw new PDOException($e);
        }


    }

    /**
     * 绑定参数
     * @param array $value
     * @throws PDOException
     */
    public function bindValue(Array $value){

        foreach($value as $key=>$val){

            // 判断是问号占位符还是命名占位符
            if(is_numeric($key)){
                $parameter = $key + 1;
            }else{
                $parameter = ':' . $key;
            }

            // 是否有第三个参数
            if (is_array($val)) {
                if (PDO::PARAM_INT == $val[1] && '' === $val[0]) {
                    $val[0] = 0;
                }
                $result = $this->statement->bindValue($parameter, $val[0], $val[1]);
            } else {
                $result = $this->statement->bindValue($parameter, $val);
            }

            // 绑定失败抛出异常
            if(!$result){
                throw new PDOException('绑定参数错误');
            }

        }

    }

    /**
     * 获取最后执行的sql语句
     * @return null
     */
    public function getLastSql(){

        return $this->sql;

    }

    /**
     * 获取最后插入数据的ID
     * @return mixed
     * @throws PDOException
     */
    public function lastInsertId(){

        return $this->getConnect()->lastInsertId();

    }

    /**
     * 开启事务
     * @return mixed
     * @throws PDOException
     */
    public function begin(){

        return $this->getConnect()->beginTransaction();

    }

    /**
     * 事务回滚
     * @return mixed
     * @throws PDOException
     */
    public function rollback(){

        return $this->getConnect()->rollback();

    }

    /**
     * 提交事务
     * @return mixed
     * @throws PDOException
     */
    public function commit(){

        return $this->getConnect()->commit();

    }

    /**
     * 获取PDO连接
     * @return mixed
     * @throws PDOException
     */
    public function getConnect(){
        $link = isset($this->links[$this->linkNum]) ? $this->links[$this->linkNum] : $this->connect();
        return $link;
    }

}